//! Simple logging that allows concurrent FS system calls.
//!
//! A log transaction contains the updates of multiple FS system calls. The logging system only commits
//! when there are no FS system calls active. Thus there is never any reasoning required about whether a
//! commit might write an uncommitted system call's updates to disk.
//!
//! A system call should call begin()/end() to mark its start and end. Usually begin() just increments
//! the count of in-progress FS system calls and returns. But if it thinks the log is close to running
//! out, it sleeps until the last outstanding end() commits.
//!
//! The log is a physical re-do log containing disk blocks.
//! The on-disk log format:
//!   header block, containing block #s for block A, B, C, ...
//!   block A
//!   block B
//!   block C
//!   ...
//! Log appends are synchronous.

const std = @import("std");
const os = @import("../os.zig");
const fs = os.fs;
const Block = fs.Block;
const Buffer = @import("Buffer.zig");

const Header = struct {
    comptime {
        std.debug.assert(@sizeOf(@This()) < @sizeOf(@FieldType(Buffer, "data")));
    }

    n: usize,
    block: [fs.LOGBLOCKS]Block,

    fn copy(dst: *Header, src: *Header) void {
        dst.n = src.n;
        @memcpy(dst.block[0..dst.n], src.block[0..dst.n]);
    }

    /// Read the log header from disk into the in-memory log header
    pub fn read(self: *Header) void {
        var buf: *Buffer = .read(start);
        defer buf.release();

        const bh: *Header = @ptrCast(&buf.data);
        self.copy(bh);
    }

    /// Write in-memory log header to disk. This is the true point at which the current transaction
    /// commits.
    pub fn write(self: *Header) void {
        var buf: *Buffer = .read(start);
        defer buf.release();

        const bh: *Header = @ptrCast(&buf.data);
        bh.copy(self);
        buf.write();
    }
};

var lock: os.Lock.Spin = .init("log");
var start: Block = undefined;

/// how many FS sys calls are executing.
var outstanding: usize = 0;
/// in commit(), please wait.
var committing: bool = false;
var header: Header = undefined;

pub fn init(sb: *fs.SuperBlock) void {
    start = .init(sb.logstart);

    // recover from log

    header.read();
    installTrans(true); // if committed, copy from log to disk
    header.n = 0;
    header.write(); // clear the log
}

/// called at the start of each FS system call.
pub fn begin() void {
    lock.acquire();
    defer lock.release();

    const p = os.proc.current().?;
    while (true) {
        if (committing) {
            p.sleep(&lock, &lock);
        } else if (header.n + (outstanding + 1) * fs.MAXOPBLOCKS > fs.LOGBLOCKS) {
            // this op might exhaust log space; wait for commit.
            p.sleep(&lock, &lock);
        } else {
            outstanding += 1;
            break;
        }
    }
}

/// called at the end of each FS system call. commits if this was the last outstanding operation.
pub fn end() void {
    lock.acquire();

    outstanding -= 1;

    if (committing) @panic("log.committing");

    var do_commit = false;
    if (outstanding == 0) {
        do_commit = true;
        committing = true;
    } else {
        // begin_op() may be waiting for log space, and decrementing log.outstanding has decreased the
        // amount of reserved space.
        os.proc.wakeup(&lock);
    }

    lock.release();

    if (do_commit) {
        // call commit w/o holding locks, since not allowed to sleep with locks.
        commit();
        lock.acquire();
        defer lock.release();

        committing = false;
        os.proc.wakeup(&lock);
    }
}

/// Caller has modified b->data and is done with the buffer. Record the block number and pin in the cache
/// by increasing refcnt. commit()/write_log() will do the disk write.
///
/// log_write() replaces bwrite(); a typical use is:
///   bp = bread(...)
///   modify bp->data[]
///   log_write(bp)
///   brelse(bp)
pub fn write(buf: *Buffer) void {
    lock.acquire();
    defer lock.release();

    if (header.n >= fs.LOGBLOCKS)
        @panic("too big a transaction");

    if (outstanding < 1)
        @panic("log_write outside of trans");

    for (0..header.n) |i| {
        if (header.block[i] == buf.block) {
            header.block[header.n] = buf.block;
            break; // log absorption
        }
    } else { // Add new block to log?
        header.block[header.n] = buf.block;
        buf.pin();
        header.n += 1;
    }
}

fn commit() void {
    if (header.n > 0) {
        writeLog(); // Write modified blocks from cache to log
        header.write(); // Wrie header to disk -- the real commit
        installTrans(false); // Now install writes to home locations
        header.n = 0;
        header.write(); // Erase the transaction from the log
    }
}

/// Copy committed blocks from log to their home location
fn installTrans(comptime recovering: bool) void {
    for (0..header.n) |i| {
        const log_block: Block = start.add(i + 1);
        var lbuf: *Buffer = .read(log_block); // read log block
        defer lbuf.release();

        var dbuf: *Buffer = .read(header.block[i]); // read dst
        defer dbuf.release();

        @memcpy(&dbuf.data, &lbuf.data);
        dbuf.write(); // write dst to disk
        if (!recovering) dbuf.unpin();
    }
}

/// Copy modified blocks from cache to log.
fn writeLog() void {
    for (0..header.n) |i| {
        const log_block: Block = start.add(i + 1);
        var to: *Buffer = .read(log_block); // log block
        defer to.release();

        var from: *Buffer = .read(header.block[i]); // cache block
        defer from.release();

        @memcpy(&to.data, &from.data);
        to.write(); // write the log
    }
}
